// clang-format off
//
// Created by goksu on 4/6/19.
//

#include <algorithm>
#include <vector>
#include "rasterizer.hpp"
#include <opencv2/opencv.hpp>
#include <math.h>
#include <functional>

static constexpr int kMaxPoint = 3;

rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    pos_buf.emplace(id, positions);

    return {id};
}

rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{
    auto id = get_next_id();
    ind_buf.emplace(id, indices);

    return {id};
}

rst::col_buf_id rst::rasterizer::load_colors(const std::vector<Eigen::Vector3f> &cols)
{
    auto id = get_next_id();
    col_buf.emplace(id, cols);

    return {id};
}

auto to_vec4(const Eigen::Vector3f& v3, float w = 1.0f)
{
    return Vector4f(v3.x(), v3.y(), v3.z(), w);
}


static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
    float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
    float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
    return {c1,c2,c3};
}


void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
    auto& buf = pos_buf[pos_buffer.pos_id];
    auto& ind = ind_buf[ind_buffer.ind_id];
    auto& col = col_buf[col_buffer.col_id];

    float f1 = (50 - 0.1) / 2.0;
    float f2 = (50 + 0.1) / 2.0;

    Eigen::Matrix4f mvp = projection * view * model;

    //SSAA
    int area = width * height;
    std::vector<std::array<Eigen::Vector3f, kMaxTotalSample>> frame_buf_ssaa(area);
    std::vector<std::array<float, kMaxTotalSample>> depth_buf_ssaa(area);
    for (int i = 0; i < area; ++i) {
        auto &frame = frame_buf_ssaa[i];
        std::fill(frame.begin(), frame.end(), Eigen::Vector3f{0, 0, 0});
        auto &depth = depth_buf_ssaa[i];
        std::fill(depth.begin(), depth.end(), std::numeric_limits<float>::infinity());
    }

    for (auto& i : ind)
    {
        Triangle t;
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
        //Homogeneous division
        for (auto& vec : v) {
            vec /= vec.w();
        }
        //Viewport transformation
        for (auto & vert : v)
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
//            vert.z() = (1.0 - vert.z()) * f1 + f2;
            vert.z() = vert.z() * f1 + f2;
        }

        for (int i = 0; i < 3; ++i)
        {
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
        }

        auto col_x = col[i[0]];
        auto col_y = col[i[1]];
        auto col_z = col[i[2]];

        t.setColor(0, col_x[0], col_x[1], col_x[2]);
        t.setColor(1, col_y[0], col_y[1], col_y[2]);
        t.setColor(2, col_z[0], col_z[1], col_z[2]);

        rasterize_triangle(t, frame_buf_ssaa, depth_buf_ssaa);
    }

    for (int i = 0; i < area; ++i) {
        depth_buf[i] = 0;
        for (int j = 0; j < kMaxTotalSample; ++j) {
            frame_buf[i] += frame_buf_ssaa[i][j];
            depth_buf[i] += depth_buf_ssaa[i][j];
        }
        frame_buf[i] /= kMaxTotalSample;
        depth_buf[i] /= kMaxTotalSample;
    }
}

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, std::vector<std::array<Eigen::Vector3f, kMaxTotalSample>>&frame_buf_ssaa, std::vector<std::array<float, kMaxTotalSample>> &depth_buf_ssaa) {
    auto v = t.toVector4();
    using Triangle3Type = std::array<Eigen::Vector3f, kMaxPoint>;
    Triangle3Type points, edges;
    for (int i = 0; i < kMaxPoint; ++i) {
        points[i].x() = v[i].x();
        points[i].y() = v[i].y();
        points[i].z() = 0;
    }
    for (int i = 0; i < kMaxPoint; ++i) {
        edges[i] = points[(i + 1) % kMaxPoint] - points[i];
    }

    auto insideTriangle = [&](float x, float y) -> bool {
        // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
        Eigen::Vector3f point(x, y, 0), t;
        t = edges[0].cross(point - points[0]);
        float z = t.z();
        for (int i = 1; i < kMaxPoint; ++i) {
            t = edges[i].cross(point - points[i]);
            if (z * t.z() < 0) {
                //不同方向
                return false;
            }
        }
        return true;
    };

    // TODO : Find out the bounding box of current triangle.
    std::vector<std::vector<float>> boundingBox(2, std::vector<float>(2));


    boundingBox[0][0] = boundingBox[0][1] = v[0].x();
    boundingBox[1][0] = boundingBox[1][1] = v[0].y();
    for (int i = 1; i < kMaxPoint; ++i) {
        for (int j = 0; j < 2; ++j) {
            boundingBox[j][0] = std::min(boundingBox[j][0], v[i][j]);
            boundingBox[j][1] = std::max(boundingBox[j][1], v[i][j]);
        }
    }



    // iterate through the pixel and find if the current pixel is inside the triangle
    float x, y;
    for (int i = static_cast<int>(boundingBox[0][0]);
        i <= static_cast<int>(boundingBox[0][1] + 1.0); ++i) {
        for (int j = static_cast<int>(boundingBox[1][0]);
             j <= static_cast<int>(boundingBox[1][1] + 1.0); ++j) {

            x = i + kStep_2;
            for (int ii = 0; ii < kMaxSample; ++ii, x += kStep) {
                y = j + kStep_2;
                for (int jj = 0; jj < kMaxSample; ++jj, y += kStep) {
                    if (insideTriangle(x, y)) {
                        // If so, use the following code to get the interpolated z value.
                        auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
                        float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                        float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                        z_interpolated *= w_reciprocal;
                        int idx = get_index(i, j);
                        if (z_interpolated < depth_buf_ssaa[idx][ii * kMaxSample + jj]) {
                            // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
//                    std::cout << z_interpolated << std::endl;
                            depth_buf_ssaa[idx][ii * kMaxSample + jj] = z_interpolated;
                            frame_buf_ssaa[idx][ii * kMaxSample + jj] = t.getColor();
                        }
                    }
                }
            }
        }
    }


}

void rst::rasterizer::set_model(const Eigen::Matrix4f& m)
{
    model = m;
}

void rst::rasterizer::set_view(const Eigen::Matrix4f& v)
{
    view = v;
}

void rst::rasterizer::set_projection(const Eigen::Matrix4f& p)
{
    projection = p;
}

void rst::rasterizer::clear(rst::Buffers buff)
{
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h);
}

int rst::rasterizer::get_index(int x, int y)
{
    return (height-1-y)*width + x;
}

void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
    //old index: auto ind = point.y() + point.x() * width;
    auto ind = (height-1-point.y())*width + point.x();
    frame_buf[ind] = color;

}

// clang-format on